Part VII
MFC and Graphics Programming

In This Part

  MFC and OpenGL 1023
  MFC and DirectX 1077
  Multimedia and MFC 1117

Chapter 29
MFC and OpenGL

by Kenn Scribner

In This Chapter

  What Is OpenGL? 1024
  Graphics Boot Camp 1025
  OpenGL Boot Camp 1032
  Minimal OpenGL Program and a Custom View 1042
  2D and 3D Models 1055
  Light 1064
  Texture Mapping 1069
  Special Effects 1073

What Is OpenGL?

In a nutshell, OpenGL is a programmer’s interface for developing interactive graphics-based applications. Although you can do simple two-dimensional (2D) work with OpenGL, it was really designed to work within the three-dimensional (3D) graphical display world. With relatively little effort on your part, you can render (draw) 3D objects and worlds, complete with lighting effects, aliasing, blending, and even fog. I’ll be discussing these terms throughout this chapter, so don’t be too concerned if you don’t understand their meaning at this point. One thing to remember is that OpenGL itself provides you with the tools you require to easily build and display complex 3D scenes. It doesn’t supply the models!


Tip:  

Though OpenGL itself doesn’t come complete with ready-made models for you to use, there is an auxiliary OpenGL library that ships with practically every OpenGL implementation, including Microsoft’s. You’ll use this library to display some stunning shapes when you reach the first sample program.


Where did OpenGL originate, and why? To answer this, step back in time to the late 1980s. At that time, Silicon Graphics (SGI) dominated the high-end graphics processing market. Its true forte was custom innovative graphics processors, which accelerated the rendering speeds of its systems. But SGI knew hardware alone wasn’t the answer. To sell systems, people needed to be able to program the processors easily. Therefore, SGI developed a graphics language, known as GL. GL was platform-specific, though, and it carried the additional burden of managing user interface elements. Although SGI wanted a language to express complex graphics, it ended up with a language that also managed mouse events and windows.

As GL grew, it became increasingly difficult to use. It wasn’t that the graphical nature of the language was so difficult; the difficulty was with managing the user interface. Because SGI systems were UNIX-based, the systems also carried SGI’s implementation of X Window (a popular open-system user interface standard still prevalent today in UNIX systems). SGI decided it was not wise to develop and maintain two parallel user interface implementations. Thus, OpenGL was born. SGI removed the user interface support from GL and forged a new graphical interface language that was lean and mean, at least when compared to its predecessor.

OpenGL is now an international standard that may be found on a wide range of systems. Windows NT and Windows 98 ship with OpenGL as a part of the basic operating system, and even Windows 95 Service Release 2 added OpenGL support as standard equipment.


Note:  

Those of you still using early releases of Windows 95 aren’t out in the cold. Microsoft released OpenGL for Windows 95 and made it freely available in a self-extracting archive. You may download it from ftp://ftp.microsoft.com/softlib/mslfiles/opengl95.exe.


So you now know OpenGL is a programmer’s interface for graphics programming, and you’ve read a bit of its history. Now I will turn to the core essence of OpenGL. If you’ve never studied the mathematics behind 3D computer graphics, you might not truly appreciate what OpenGL does for you (don’t let that stop you from using it, however). The mathematics involves several successive transformations using transformation matrices, which naturally involve matrix algebra. You’ll see a bit of this in a moment when you look at some of the most basic graphics concepts. OpenGL, however, manages much of the mathematics for you, allowing you to concentrate on the real problem at hand—your graphical scene. OpenGL, then, is really a mathematical state machine that captures the math behind graphics programming as well as some of the other basic fundamentals, such as double buffering (writing to separate graphics memory buffers for animation purposes). If you have one of the newer-generation 3D graphics accelerator video cards, its hardware probably supports OpenGL and the processing (and memory management) OpenGL must undertake to successfully render a scene even more quickly.

Graphics Boot Camp

This is a book about programming, but I’d be remiss if I didn’t spend some time describing some core terms and key concepts you absolutely must understand before you attempt your first 3D masterpiece. You’ll begin by exploring some fundamental graphics programming terms and then you’ll move into some basic theory.

Core Terms and Key Concepts

To begin your look at OpenGL, you need to have a good understanding of the graphics programmer’s language. Not their programming language, such as C++, but rather the way they speak. When they talk about their models and translations, you need to understand what they are talking about. So I will start with some core terms.

Model. When I talk about a model, I’m talking about an individual object within my (typically) 3D scene. Perhaps I have a model of a cube, or maybe a sphere. I could even have a model of a 3D Tyrannosaurus rex.

Polygons. A 3D surface appears to be solid, but it is really composed of several shapes (usually triangles for theoretical reasons). The triangles are arranged in a mesh, and the area within the polygon is left alone (wireframe), shaded (colored in, with or without lighting effects), or texture-mapped (external image applied). Wireframe is just that—the model is drawn using only lines. If you apply shading, or light properties, you fill in the area of the polygon with a color. If the polygons are numerous enough, and if your system has a large color palette, you can render terrific-looking, highly detailed models. The ultimate in polygon surface effects is texture-mapping, where you apply a decal (texture) to the polygon. The decal is typically a bitmapped image you create beforehand. You conform (distort) the decal to the polygon surface, thus making the polygon itself appear to be much more complex than it really is.

Normals. The polygons OpenGL accepts lie in a plane (the vertices are coplanar). The polygon’s surface normal is a vector that resides in the third coordinate plane tangent to the surface of the polygon. Imagine that the top of your desk is a polygon. If you were to stand a sharpened pencil eraser down on the desk, the pencil would represent the desktop’s polygonal (surface) normal. Normals, and their proper calculation, are critical for light calculations.

Translation. Translation means moving the model (intact) from one set of coordinates to another. Essentially, you’re relocating your model to another location within the scene.



Scaling. When you scale a model, you’re adjusting its size. That is not to say you must adjust all aspects of its size at the same time. You could, for example, scale in only one of the three coordinate axes (the x-axis, the y-axis, or the z-axis), thus stretching the model in one direction only to distort it from its original shape.

Rotation. Just as you can translate and scale, you can also rotate about a (arbitrary) coordinate axis. In effect, you spin the model about some given longitudinal axis.

Viewing frustrum All your models (and the scene as a whole) will be contained in an imaginary area called the viewing frustrum (see Figure 29.1). The frustrum has six sides, each known as a clipping plane. If any part of your model is rendered outside this imaginary area, the part of the model outside will be summarily cut off (clipped) and not shown. The frustrum may be either an orthographic (parallel) or a projection frustrum. If the frustrum is parallel, objects will be rendered without regard to depth. You will have, effectively, a 2D scene. On the other hand, projected frustrums implement perspective foreshortening by using the depth information. Distant objects appear distant because they are scaled appropriately. The more distant they are, the smaller they appear.


Figure 29.1  The viewing frustrum.

Light. Light in graphical terms is composed of many parts. First, there is ambient light, which is the natural light present in the 3D scene without any additional light source. Sunshine would be considered ambient lighting. Then there is the light reflected from the surface of the object, which is called diffuse light. The combination of ambient and diffuse light makes your scene appear brighter or dimmer depending upon the relative strength of each component. These light components typically provide the object with its color. Specular light, however, is light that reflects from the object’s surface due to a focused light source. This light component is usually composed of the light source’s color. If you imagine a red sphere, you see a red sphere because it reflects red light (it absorbs the rest of the color spectrum). If you were to shine a bright yellow light onto the surface of the sphere, you would see a red sphere with a bright yellow spot on the surface. This yellow spot is the specular light component.

Material properties. Each object, if it is rendered with light computations, will have material property assignments. These properties denote the object’s color properties—how much light it reflects, how it handles specular light, how shiny it is, and so on. By adjusting an object’s material properties, you determine how the object will appear given certain light conditions. It could look shiny, like plastic or chrome, or it could appear dull, like mud.

You’ll be seeing these terms and concepts throughout this (and the next) chapter. Now that you have a common foundation of terms and concepts, it is time for an introduction to the mathematics of graphics programming. Because OpenGL provides you with a rich mathematics library, I believe you must understand what’s under the hood to properly use the tool.

The Basic Mathematics of Graphics Programming

My goal with this section is not to scare you away from 3D graphics. Rather, it’s important for you to have some appreciation, if not a fundamental understanding, of what is happening within the bowels of OpenGL to use it effectively. I can’t possibly go into enough detail here to derive the equations and matrices you’ll see, but at least having seen them, you’ll understand why OpenGL does some things the way it does and why you work with OpenGL the way you do.

To begin, you will be working in a 3D coordinate space where the three axes of the space are orthogonal. That is, they are all at right angles (90 degrees to each other). The fundamental meaning of this is that you didn’t bend your 3D universe in some fashion. The result of this is that the math becomes very much simpler. Figure 29.2 gives you the basic idea.


Figure 29.2  3D coordinate axes.

All your 3D models will be described using the three basic coordinates—x, y, and z. You also may see in Figure 29.2 the coordinate axes’ unit vectors. A vector is a directed value. That is, it has a direction and a magnitude. When you drive a car, your speed is considered a scalar value. It is simply a number. But when you combine your speed with a given direction (say, northeast), you know your car’s velocity, which is a vector quantity. It has magnitude (the speed component) and direction (northeast). It will be the same when you deal with 3D space. You may know a particular scalar value, such as the length of the side of a cube, but it isn’t as informative if you don’t know how the side of the cube is aligned because you lack the directional component.

The vectors shown in Figure 29.2 are known as unit vectors. That means nothing more than their magnitude (scalar) value is one (1). This also simplifies the mathematics in many cases. When you deal with light calculations in particular, you’ll be interested in normalizing your vectors. You’ll reduce their magnitude to the value one, again to simplify the mathematics. I’ll refer back to this in a moment.

A single vector isn’t particularly interesting. In essence, you have a line. It becomes far more interesting when you have two vectors that cross each other at a given point (called a vertex). Each vector has a magnitude and direction, so if you were to add them, in vector terms, you would find that you have a third vector (see Figure 29.3).


Figure 29.3  Vector addition and the resultant vector.

This vector, the result of the addition of the former two vectors, is the resultant vector. It essentially is a conglomerate of the magnitudes and directions of the first two vectors. I’ll revisit these vectors shortly when I again talk about polygonal surfaces and surface normals.

Though some graphics students might argue this point, when you deal with vectors it sometimes simplifies life if you work the mathematics using matrices. The rows and columns of the matrices either represent the vectors you’re interested in or they represent a transformation you want to apply to a coordinate system (all points within that system, as represented by the coordinate unit vectors). As an example, think about translation. Translation is the act of moving an object in 3D space from one location to another. If you consider your objects to be composed of vectors, that is, edges that connect the vertices of the polygons that compose the object, you run each edge vector through the translation matrix to calculate the new location for the vector (see Figure 29.4).


Figure 29.4  A translation matrix equation.

What the equation tells you is this: If you know a starting point and the amount you want to translate that point, you can use this matrix to relocate the given point. All the points in your model will be translated in the same manner. Other transformation matrices exist for scaling and rotation as well.



I mentioned previously the concept of normalization. Actually, when dealing with computer graphics, this term has two meanings. The first is to take a vector of some arbitrary size and make its scalar value (its length, so to speak) one. The other meaning is to determine the surface normal to a given coplanar polygon. If you have a polygon, and if the vertices of the polygon lie in a single plane (it isn’t twisted), you have a coplanar polygon. But as you know, a plane is a 2D construction. What about the third dimension? In most cases, you ignore the third dimension when thinking about polygons, except when you have a vector that is perpendicular to the polygon (perpendicular means the vector rises 90 degrees from the surface of the polygon). This 90-degree vector is the polygon’s surface normal, and it is critical for light calculations. You’ll see why that is later in this chapter.

In many cases you will intuitively know what the surface normal vector should be for a given polygon. In terms of OpenGL, imagine you have this polygon:

glBegin(GL_POLYGON);
   glVertex3f(5.0f,5.0f,5.0f);
   glVertex3f(-5.0f,5.0f,5.0f);
   glVertex3f(-5.0f,-5.0f,5.0f);
   glVertex3f(5.0f,-5.0f,5.0f);
glEnd();

This forms a square you’ll see as if you were directly on top of it (your eye would be looking down upon it). The polygon’s plane is evident—see the z coordinate values? They’re all 5.0, which automatically tells you this polygon lies in the z plane. Therefore, the surface normal must be composed entirely of the z coordinate (it will have no x or y coordinate values). Again in terms of OpenGL code, the surface normal would be declared like this:

glBegin(GL_POLYGON);
   glVertex3f(5.0f,5.0f,5.0f);
   glVertex3f(-5.0f,5.0f,5.0f);
   glVertex3f(-5.0f,-5.0f,5.0f);
   glVertex3f(5.0f,-5.0f,5.0f);
   glNormal3f(0.0f,0.0f,1.0f);
glEnd();

There are two things you should note here. First, the surface normal is assigned within the same glBegin()/glEnd() pair as the polygon’s vertices. This is how OpenGL associates this particular normal with this polygon. The second thing you should see is that the value of the z component of the normal vector is exactly one. The surface normal vector itself is normalized. This particular polygon is somewhat special in that it lies in the z plane. An infinite number of other polygons do not lie in the z plane, so don’t be fooled into thinking that a surface normal vector will always have a couple of zeros and a one for vector components! It will always be true, however, that the length of the surface normal vector will be one.

Hard-coding the normal is fine if you are able to predetermine it. I’ll now show you what to do if you have some arbitrary polygon in 3D space. Basically, you take two of the polygonal edges (represented as vectors) and take their cross product. The mathematics is such that when you take the cross product of two vectors, you get a normal vector as a result. In the case of graphical surface normals, you also must normalize the length of the resulting vector. Listing 29.1 gives you the code to take a cross product and normalize the result, which I took from this chapter’s GLObject sample program. I’ll leave it to you to trace through the code if you’re unfamiliar with vector cross-product calculations. However, you should know that the input vectors were defined in a counterclockwise fashion. This is essential for the cross product to yield the proper result (the resulting vector will be in the wrong direction if you do it incorrectly!).

Listing 29.1 Vector Cross-Product Calculations


void CGLObjectView::Normal( GLfloat* pt1,
                            GLfloat* pt2,
                            GLfloat* pt3,
                            double* nml )
{
   // Form two vectors from the given input points
   double v1[3], v2[3];
   v1[0] = pt2[0] - pt1[0];
   v1[1] = pt2[1] - pt1[1];
   v1[2] = pt2[2] - pt1[2];
   v2[0] = pt3[0] - pt1[0];
   v2[1] = pt3[1] - pt1[1];
   v2[2] = pt3[2] - pt1[2];

   // Take their cross product
   nml[0] = v1[1] * v2[2] - v1[2] * v2[1];
   nml[1] = v1[2] * v2[0] - v1[0] * v2[2];
   nml[2] = v1[0] * v2[1] - v1[1] * v2[0];

   // Normalize the resultant
   double dLen = sqrt(nml[0]*nml[0] ++
                      nml[1]*nml[1] ++
                      nml[2]*nml[2]);
   if ( dLen != 0.0 ) {
      nml[0] = nml[0] / dLen;
      nml[1] = nml[1] / dLen;
      nml[2] = nml[2] / dLen;
   } // if
}

To use this cross-product function, you pass in a number of double arrays. The first three are filled with the points that define the two vectors. The last is a result array that will contain the normal vector. You pass this to OpenGL when you specify the polygon’s surface normal.

You’ve completed your tour of 3D computer graphics theory! I’m sure you’ll be amazed by OpenGL’s capability, especially knowing not only how complicated 3D graphics can be, but also knowing that the sheer number of calculations can be staggering for even trivial scenes.

OpenGL Boot Camp

Now it is time to turn to OpenGL to see how it mechanizes the 3D rendering process. In some fashion, OpenGL supports all the terms, concepts, and mathematics you’ve just examined. The beauty of OpenGL is you needn’t concern yourself with the details. You need to understand the fundamentals to use this tool properly, but the details lie within the OpenGL implementation itself.

But just as OpenGL has tremendous beauty, I would be doing you a disservice if I didn’t also mention OpenGL’s warts. OpenGL was originally implemented in the UNIX environment, and if you’ve worked in both Windows and UNIX, you’ll find that OpenGL still has a “UNIX-like” feeling to it. OpenGL simply wasn’t developed within the Windows framework, so it doesn’t have a natural feel within Windows. Why would I claim this? For one, OpenGL has its own breed of device context (the Windows version of which you looked at in Chapter 4, “Painting, Device Contexts, Bitmaps, and Fonts”). You still require a Windows-style device context, but you must also manage an OpenGL context as well. Further, there can be only one active OpenGL device context per thread. This isn’t necessarily a problem, but you must be aware of this limitation and code accordingly.

OpenGL also attempts to be platform-independent, so there are OpenGL-specific definitions for things as basic as the floating-point datatype. In C++, you would declare a floating-point variable in this manner:

float fMyVar;

However, when using OpenGL, you would use a GLfloat variable:

GLfloat fMyGLVar;

For all intents and purposes, both variables are floating-point variables. They may, in fact, be the same. But your OpenGL code is now more portable, at the cost of ease of programming.

So, now that you understand OpenGL is a new programming interface with its own datatypes, I will introduce you to some OpenGL conventions you’ll be using throughout this chapter. I’ll start with the custom datatypes I just mentioned.



OpenGL Datatypes

As you’ve seen, 3D graphics programming is by nature mathematically intense, so it may not be too surprising to find that OpenGL has quite a number of custom datatypes geared towards mathematics and 3D graphics calculations. Table 29.1 lists the OpenGL datatypes and their equivalent C++ types.

Table 29.1 OpenGL Datatypes with C++ Equivalents

OpenGL Datatype C++ Datatype

GLbyte signed char
GLubyte unsigned char
GLboolean unsigned char
GLshort short
GLushort unsigned short
GLint long
GLuint unsigned long
GLsizei long
GLfloat float
GLclampf float
GLdouble double
GLclampd double
GLenum unsigned long
GLbitfield unsigned long
GLvoid void
HGLRC HGDIOBJ

By convention you’ll use these datatypes when programming in OpenGL. Anyone reading your code who is familiar with OpenGL will expect this convention to be followed. But another good reason for using the datatype mappings is that the mappings could conceivably change given future microprocessors (say, for 64-bit machines). If you follow this convention, the burden of correcting the datatypes lies with the OpenGL vendor, not you. You simply recompile your code and continue selling your product.

OpenGL also follows its own convention when referring to such things as arrays. In OpenGL, you don’t have an array of GLfloats. Rather, you have a vector of GLfloats. You will frequently need to pass an array of floats (or ints, or whatever) as a parameter to a given OpenGL function. In OpenGL terms, you are passing in a vector, even though the data may have nothing to do with an actual (mathematically based) vector. This distinction becomes important when I discuss the OpenGL function naming convention.

OpenGL Function Naming Conventions

When working with traditional Windows programs, many programmers prefer to embrace the Hungarian variable naming convention. For example, if a variable is an integer, its variable name will be prefaced with the letter i. OpenGL follows a similar vein, only not with variable types (it has its own), but rather with function names. Almost all OpenGL functions accept integer, float, and double variable datatypes, and many accept vectors of the same. To distinguish the function that accepts integers from the equivalent function that accepts doubles, the datatype is concatenated to the end of the function name. For example, consider this OpenGL function call (which establishes a vertex, or endpoint):

void glVertex2f(GLfloat x, GLfloat y);

You can see that it is an OpenGL function because it begins with the letters gl. You know its basic functionality is to assign vertex points. And you also know the function accepts two parameters, each being a float value. The equivalent function, using a vector, looks like this:

void glVertex2fv (const GLfloat *v);



Now the parameter indication is more interesting. You know the input parameter is a vector that contains two float values, which is something you don’t normally know when passed a simple array of floating-point variables. The vector passed to the OpenGL function may include more members than the function will use, but it may not contain less or OpenGL will return with catastrophic results. Using OpenGL’s vertex assignment function as an example, Table 29.2 shows you the function name with the corresponding expected datatypes. These datatypes are typical for almost all OpenGL functions (directly from gl.h).

Table 29.2 OpenGL Vertex Function Names and Parameter Lists

Vertex Function Name Parameter List

glVertex2d GLdouble x, GLdouble y
glVertex2dv const GLdouble *v
glVertex2f GLfloat x, GLfloat y
glVertex2fv const GLfloat *v
glVertex2i GLint x, GLint y
glVertex2iv const GLint *v
glVertex2s GLshort x, GLshort y
glVertex2sv const GLshort *v
glVertex3d GLdouble x, GLdouble y, GLdouble z
glVertex3dv const GLdouble *v
glVertex3f GLfloat x, GLfloat y, GLfloat z
glVertex3fv const GLfloat *v
glVertex3i GLint x, GLint y, GLint z
glVertex3iv const GLint *v
glVertex3s (GLshort x, GLshort y, GLshort z
glVertex3sv const GLshort *v
glVertex4d GLdouble x, GLdouble y, GLdouble z, GLdouble w
glVertex4dv const GLdouble *v
glVertex4f GLfloat x, GLfloat y, GLfloat z, GLfloat w
glVertex4fv const GLfloat *v
glVertex4i GLint x, GLint y, GLint z, GLint w
glVertex4iv const GLint *v
glVertex4s GLshort x, GLshort y, GLshort z, GLshort w
glVertex4sv const GLshort *v

OpenGL Render Function Scoping

Because OpenGL manages a graphical state, it must be told when a given set of operations is related. That is, just as you use the left and right brace in C++ to indicate a single C++ statement, you enclose many OpenGL function calls within glBegin() and glEnd(). For example, you could use this OpenGL code to draw a 2D square:

void DrawGLSquare() {
   glBegin(GL_LINES); // square in X-Y plane
      glVertex3f(-5.0f, 5.0f, 0.0f);
      glVertex3f(-5.0f, -5.0f, 0.0f);

      glVertex3f(-5.0f, -5.0f, 0.0f);
      glVertex3f(5.0f, -5.0f, 0.0f);

      glVertex3f(5.0f, -5.0f, 0.0f);
      glVertex3f(5.0f, 5.0f, 0.0f);

      glVertex3f(5.0f, 5.0f, 0.0f);
      glVertex3f(-5.0f, 5.0f, 0.0f);
   glEnd();
}

With this code you are telling OpenGL to draw lines (GL_LINES) from the first vector coordinate to the second (as indicated by their grouping). In Win32 GDI terms, the first glVertex3f() call performs the MoveToEx(), and the second performs the LineTo(). The first glVertex3f() pair then draws a line from (-5.0,5.0) (in (X,Y) format) to (-5.0,-5.0). The remaining groups draw the other three sides of the square. You’ll revisit this code when you look at the GLMin sample program shortly. Note that some OpenGL commands are excluded from the glBegin()/glEnd() pair, so be sure to see the online help for more details.


Note:  

It is no accident that the vertices are drawn in counter-clockwise fashion. This is critical for cross-product calculations, which are used when rendering the square with light effects.




The OpenGL Matrix Stack

As you have seen, OpenGL uses matrices to manage the mathematical calculations required to render your 3D scene. To make the matrix manipulations much easier, OpenGL provides a matrix stack. The matrix stack operates like any other stack-based scheme. You first push a matrix onto the stack, which effectively saves the current matrix contents. You are then free to manipulate the matrix as required to render a given object or your entire scene. Finally, you pop the original matrix off the stack to make more manipulations.

This feature is most useful when rendering many 3D objects in a single scene. Typically, you’ll prepare a matrix for initial use by making several OpenGL function calls. The stack-based approach allows you to establish your basic matrix a single time while reusing it as much as you require. For example, you could easily draw two squares in different locations using the same code I presented for drawing a single square:

void RenderView() {
   glPushMatrix();
DrawGLSquare();
glPopMatrix();

   glPushMatrix();
      glScalef(5.0f,5.0f,0.0f);
      glRotatef(45.0f,0.0f,0.0f,1.0f);
      DrawGLSquare();
   glPopMatrix();
}

In this case, the matrix was (presumably) initialized prior to the first glPushMatrix() call. Although in this example I didn’t actually modify the matrix in any way for the first square, I could have. In fact, it’s a good habit to get into, as matrix bugs are difficult to track down. Everything will seem to work correctly, yet your final scene is not rendered correctly. In any case, I save the matrix on the stack, render the first square, then pop the matrix off the stack to be used again.

The second square, however, does modify the matrix. As you’ve seen, whenever you translate, rotate, scale, or otherwise manipulate the model view, you mathematically change the model view matrix. Here, I first scale the model view matrix, and then rotate it. The resulting square is reduced to one-half of its original size, then rotated about the z-axis 45 degrees. The resulting scene is shown in Figure 29.5. The second square is the diamond-shaped object in the center of the first square. This also demonstrates a powerful feature OpenGL provides you. Notice that I manipulated the model view matrix, but I used the same model code (DrawGLSquare()). The objects in your model code are placed within your scene according to the resulting transformational matrix, so you may code your models as if they were centered at (0,0,0). This makes model development tremendously easier.


Figure 29.5  GLMin program output.

The OpenGL Rendering Context

As you saw in Chapter 4, Windows applications (MFC-based or otherwise) use a device context for graphical output. The Windows GDI manages device contexts and provides the functions you use to manipulate the images within those device contexts. OpenGL does not use the standard Windows device context, however. It uses a rendering context.

Like device contexts, rendering contexts maintain a graphical state. If you use the SelectObject() GDI function to select a new drawing pen into a device context, that pen remains active in that device context until you select in another. OpenGL’s rendering context is similar. OpenGL uses the rendering context to maintain the information it requires to properly render a scene within a given window. In fact, this is the main purpose behind a rendering context. OpenGL must have a mechanism for associating its output with a Windows device context. By storing certain pieces of information within a rendering context, OpenGL is able to update (more or less portably) a window’s graphical state in the Windows family of operating systems. This relationship is shown in Figure 29.6.


Figure 29.6  Device context/rendering context relationship.

Rendering contexts are thread-safe, which means multiple threads can use a single rendering context. You may have several rendering contexts stored within your application, thus allowing you to easily select the context you require quickly. However, there cannot be more than one active rendering context per thread at any given time. Moreover, there must be a current, active rendering context per thread for you to successfully process OpenGL commands.

Unlike Windows GDI functions, OpenGL commands do not require a handle or pointer to a rendering context. Whichever rendering context is currently active will accept all processed OpenGL commands. The rendering context is implicit in the drawing command calls.

Rendering context management requires at a minimum the use of 3 of the 16 Windows-specific OpenGL function calls (the so-called “wiggle” functions). Table 29.3 outlines these. To use OpenGL, you first create a rendering context, initialize it, make it current, use it, make it not current, and then destroy it. This is not to say that you do this each and every time you draw your scene! Rather, you first create and initialize the rendering context, and then reuse it as long as your application is active (switching it for the current and noncurrent rendering context any number of times), and then destroy it when your application exits.

Table 29.3 Primary Windows-Specific OpenGL Commands

Return Value Command

HGLRC wglCreateContext(HDC hDC)
BOOL wglDeleteContext(HGLRC hRC)
BOOL wglMakeCurrent(HDC hDC, HGLRC hRC)

You create a rendering context using wglCreateContext(), delete one using wglDeleteContext(), and make a particular rendering context the current context by using wglMakeCurrent(). The remaining Windows-specific OpenGL function calls are mainly convenience functions for retrieving information regarding the rendering context/device context mapping, or for setting specific (advanced) behaviors beyond the scope of this discussion.



At the heart of this context mapping is the format of a pixel. That is, OpenGL and Windows don’t necessarily agree with Windows regarding pixel format, at least as a default condition. When you initialize OpenGL, what you will really be doing is moderating a negotiation process between what OpenGL desires and what the given Windows system can provide. The outcome of this negotiation process will be a rendering context that properly maps what OpenGL provides to what the Windows system is capable of displaying.

You will use several GDI functions to handle this negotiation process, which you’ll examine in more detail later in this chapter. These functions, however, all require a PIXELFORMATDESCRIPTOR structure, the definition for which is shown in Listing 29.2 (taken from wingdi.h).

Listing 29.2 PIXELFORMATDESCRIPTOR Definition


/* Pixel format descriptor */
typedef struct tagPIXELFORMATDESCRIPTOR
{
    WORD  nSize;
    WORD  nVersion;
    DWORD dwFlags;
    BYTE  iPixelType;
    BYTE  cColorBits;
    BYTE  cRedBits;
    BYTE  cRedShift;
    BYTE  cGreenBits;
    BYTE  cGreenShift;
    BYTE  cBlueBits;
    BYTE  cBlueShift;
    BYTE  cAlphaBits;
    BYTE  cAlphaShift;
    BYTE  cAccumBits;
    BYTE  cAccumRedBits;
    BYTE  cAccumGreenBits;
    BYTE  cAccumBlueBits;
    BYTE  cAccumAlphaBits;
    BYTE  cDepthBits;
    BYTE  cStencilBits;
    BYTE  cAuxBuffers;
    BYTE  iLayerType;
    BYTE  bReserved;
    DWORD dwLayerMask;
    DWORD dwVisibleMask;
    DWORD dwDamageMask;
} PIXELFORMATDESCRIPTOR, *PPIXELFORMATDESCRIPTOR,
  FAR *LPPIXELFORMATDESCRIPTOR;

This large and admittedly complicated structure carries the specific system information between OpenGL and the particular Windows system device context on which your application is running. Between any two systems, the given capabilities of each, as expressed in device context terms (number of color planes, video memory, acceleration, and so on), are dependent upon the video hardware and drivers each of the systems is using. Some systems are capable of supporting OpenGL, and some are not, though it would be rare given the advanced features of contemporary hardware to lack support for OpenGL. In any case, you use the PIXELFORMATDESCRIPTOR structure to record the settings you require OpenGL to support, such as double buffering (using two video buffers to support flicker-free animation) and RGBA pixel types (red, green, blue, and alpha, which is used for blending effects like fog). You then pass the structure to a GDI function to assess the capabilities of your current system. Assuming your system is capable of supporting the needs of OpenGL, you advance to actually creating a rendering context using wglCreateContext(). You’ll look at this process in the next section.

Minimal OpenGL Program and a Custom View

You have now completed your introduction to OpenGL, as well as to computer graphics in general. It’s now time to turn to integrating OpenGL into the MFC framework. This isn’t difficult, but I will take an approach I find helpful to make it even easier. I will encapsulate the OpenGL functionality I require into a new view class. Then, when I require an OpenGL view, I’ll derive my new view from the OpenGL base class that I will create in this section. Some editing will be required to integrate this OpenGL view class into the basic MFC AppWizard source code Developer Studio provides you, but that integration is minimal when compared to re-creating this base class each time you want to use OpenGL.

The steps required to run OpenGL from your MFC application are as follows:

  Create your basic MFC application using the MFC AppWizard.
  Add the custom view class you will create in this section to your project.
  Integrate the custom view class into your application.
  Build your 3D scene.
  Add the OpenGL libraries to your project’s link list.


Note:  

This technique works for both single document (SDI) and multiple document interface (MDI) applications. I have included sample applications to demonstrate both situations (GLMin and GLMinMDI).


Creating Your Basic MFC Application

There is no special OpenGL requirement when creating your basic application framework. However, the GLView base class inherits directly from CView. If you use a view base class other than CView, you will need to create your own OpenGL-based class to act as the base class for the view you selected. That is, if you create an application that implements a CFormView view class, you will need to create a CGLFormView class to manage the OpenGL aspects of your application.

Adding the Custom OpenGL Base Class

Adding the OpenGL view class, CGLView, to your project is simple. Select Project, Add To Project, Files from the Developer Studio menu, then select GLView.cpp and GLView.h from the resulting file dialog and click OK. After the files have been added to your project, you can integrate them into your overall application.

This, however, presupposes that you have the CGLView class, so now is a good time to examine the mechanisms for running OpenGL from within a view. To begin, you must initialize OpenGL and its rendering context, modify your view’s style bits, manage the lifetime of any custom palette you create, handle window sizing issues, and clean up when the view is destroyed. In effect, this means you must do something in each of these MFC methods:

CGLView::PreCreateWindow()
CGLView::OnCreate()
CGLView::DestroyWindow()
CGLView::OnSize()
CGLView::OnEraseBkGnd()

You will also need to include the OpenGL header files and libraries, and you may need to worry about palette management if your application is running on a system with a less capable video card. In any case, I will now describe each method individually.

View Precreation

OpenGL requires the style bits of the enclosing view to be modified to clip child and sibling windows. In effect, OpenGL requires your view to exclude the area occupied by child windows when drawing within the parent window (WM_CLIPCHILDREN) and clips all of the child windows relative to each other (WM_CLIPSIBLINGS). In this manner, OpenGL doesn’t paint outside of its window. Listing 29.3 shows how I implemented this in CGLView.

Listing 29.3 OpenGL Window Style Bits and Window Precreation


BOOL CGLView::PreCreateWindow(CREATESTRUCT& cs)
{
   // Set up style to suit OpenGL
   cs.style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
   return CView::PreCreateWindow(cs);
}

Note that I logically OR the new style bits into the creation structure’s style bits member. This modifies these particular bits without disturbing the existing bits. After I’ve adjusted the style bits, I need to handle the creation of the window.



View Creation

Creating an OpenGL view is one of those things that looks more difficult than it really is, primarily because you have to both initially tie a new rendering context to a device context and twiddle the PIXELFORMATDESCRIPTOR bits. As it happens, when you have code to do this, you’ll probably find yourself using the same code over and over (in fact, I’ll create a custom view class to do this for you in an upcoming section of this chapter).

Creating an OpenGL view-based application starts essentially the same as the creation of any other view. Windows, through MFC, will send a WM_CREATE message down through the message map chain. And as with any other Windows message, you elect to add a handler to process that specific message if you want to tailor your application’s behavior from the normal (default) Windows behavior. In this case, you must add a handler for WM_CREATE, and when you intercept that message, you initialize OpenGL. Listing 29.4 shows you the code I use to do just this.

Listing 29.4 OpenGL WM_CREATE Handler


int CGLView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   if (CView::OnCreate(lpCreateStruct) == -1)
      return -1;

   // Initialize OpenGL parameters
   PIXELFORMATDESCRIPTOR pfd = {
      sizeof(PIXELFORMATDESCRIPTOR), // Structure size
      1,                             // Version number
      PFD_DRAW_TO_WINDOW |           // Property flags
          PFD_SUPPORT_OPENGL |
          PFD_DOUBLEBUFFER,          // (remove if no double buf)
      PFD_TYPE_RGBA,                 // PixelType
      24,                            // 24-bit color
      0, 0, 0, 0, 0, 0,              // Color bits and shift
      0, 0, 0, 0, 0, 0, 0,           // Alpha and accum buffer bits
      32,                            // 32-bit depth buffer
      0, 0,                          // No stencil or aux buffer
      PFD_MAIN_PLANE,                // Layer type
      0,                             // Reserved
      0, 0, 0                        // Unsupported
   };

   // Tell GDI to convert device context from Win32 to
   // OpenGL.
   CClientDC dcClient(this);
   int iPixelFormat = ChoosePixelFormat(dcClient.m_hDC,&pfd);
   if ( !iPixelFormat ) {
      // This system cannot run OpenGL
      TRACE(“Error retrieving pixel format index...\n”);
      ASSERT(FALSE);
      AfxMessageBox(“Cannot initialize OpenGL...quitting.”,
                    MB_OK | MB_ICONERROR);
      return -1; // will fail new document creation...
   } // if

   if ( !SetPixelFormat(dcClient.m_hDC,iPixelFormat,&pfd) ) {
      // This system cannot run OpenGL
      TRACE(“Error setting new pixel format...\n”);
      ASSERT(FALSE);
      AfxMessageBox(“Cannot initialize OpenGL...quitting.”,
                    MB_OK | MB_ICONERROR);
      return -1; // will fail new document creation...
   } // if

   // Update the PIXELFORMATDESCRIPTOR structure once
   // the device context has been modified.
   DescribePixelFormat(dcClient.m_hDC,iPixelFormat,
                       sizeof(pfd),&pfd);

   // The PIXELFORMATDESCRIPTOR has been updated, so you now
   // determine whether to create and manage a custom
   // palette.
   if ( pfd.dwFlags & PFD_NEED_PALETTE ) {
      // You do, so build a new palette...
      SetupPalette();
   } // if

   // Create the OpenGL rendering context
   m_hRC = wglCreateContext(dcClient.m_hDC);
   if ( m_hRC == NULL ) {
      // This system cannot run OpenGL
      TRACE(“Error creating the rendering context...\n”);
      ASSERT(FALSE);
      AfxMessageBox(“Cannot initialize OpenGL...quitting.”,
                    MB_OK | MB_ICONERROR);
      return -1; // will fail new document creation...
   } // if

   // You now make it the current rendering context so
   // you might set your clear color
   wglMakeCurrent(dcClient.m_hDC,m_hRC);
   glClearColor(0.0f,0.0f,0.0f,0.0f); // black
   wglMakeCurrent(dcClient.m_hDC,NULL);

   return 0;
}

The very first thing I do is allow the base class to handle window creation in the normal fashion:

if (CView::OnCreate(lpCreateStruct) == -1)
      return -1;

If this code isn’t successful, you won’t need to initialize OpenGL because you’ll have no window.

Then, I create a PIXELFORMATDESCRIPTOR structure and fill in its data members. You examined those members in the previous section. Here, I actually assign them values, with the overall goal being that OpenGL ultimately must provide its graphical output to a device context. But because device contexts are Windows-specific (not platform independent), OpenGL requires an intermediate initialization step to allow fast conversion from its rendering system to the particular platform (in this case, Windows). The PIXELFORMATDESCRIPTOR facilitates this initialization.

After I’ve established the PIXELFORMATDESCRIPTOR settings I want, I begin the process of building the OpenGL rendering context. First, I query the Windows device context for its pixel format information. SetPixelFormat() tries to match the closest system pixel format (as specified by the PIXELFORMATDESCRIPTOR structure), and if it finds a suitable match, returns a pixel format index that is the closest match. If I find no match, I terminate the application with a brief error dialog.

What I have now is the closest match pixel format index for this device context. What I want is a precise match (at least to a first approximation), so I use the SetPixelFormat() function to establish the pixel format I want. If this fails, I terminate the application with the same informative error dialog.

If I successfully established the new pixel format settings, I now need to retrieve the settings that actually took effect. With luck, they match those I requested. However, if the application initialized successfully to this point, OpenGL will work within my application’s framework. I use the DescribePixelFormat() function to retrieve the now-current pixel format settings.

At this time, I elect to create a custom palette if one is required. I’ll know if I require one by querying the PIXELFORMATDESCRIPTOR structure’s dwFlags attribute. If the PFD_NEED_PALETTE flag is set, I know the system does not support full 24-bit color and will require a custom palette. If this is the case, I create a palette using a special function I provide for this purpose (the details of which are left to the reader to discern; they are not particularly interesting to OpenGL discussions).

Finally, I arrive at the really interesting part. I create the OpenGL rendering context using wglCreateContext(). wglCreateContext() accepts a device context handle as input and returns to you a handle to an OpenGL rendering context that matches the device context. Given this, you’re now free to make other OpenGL command calls, which I do by setting the view clear color (the default background color).

When I mentioned previously that the OpenGL rendering context is directly mapped to a Windows device context, the wglMakeCurrent() function is the call that does this. Before I set the clear color, I make the rendering context I just created the current rendering context for this thread. When I do, I also provide a device context, which is the device context the rendering context will use to map OpenGL commands to the window in which the device context is assigned. After I set the clear color, using glClearColor(), I disassociate the rendering context from the device context by again calling wglMakeCurrent(). In this case, I set the rendering context to NULL. At this point, OpenGL cannot process drawing commands unless and until I make the rendering context current again.



View Destruction

Just as you had to create the OpenGL rendering context, you also must destroy it (or them, if you created multiples). You could destroy it in one of several places (such as after you’re through using it), but I find it best to keep at least one in memory until my application either receives the WM_DESTROY message or CWnd::DestroyWindow() is called. In the case of a CView-derived class, I use the latter. Listing 29.5 shows the code I use to destroy the rendering context. Here, I first delete the context by calling wglDestroyContext() with the particular rendering context supplied as a parameter value. Then, if I happen to have created a custom palette, I use the GDI function DeleteObject() (through CWnd) to actually delete that too. Finally, I call my base class, which in this case is CView, to finalize the window’s destruction.

Listing 29.5 OpenGL View Destruction


BOOL CGLView::DestroyWindow()
{
   // Shut down OpenGL by deleting the rendering context
   wglDeleteContext(m_hRC);

   // Delete your custom palette, if any was created
   if ( m_hPalette != NULL ) {
      DeleteObject(m_hPalette);
   } // if

   return CView::DestroyWindow();
}

If you fail to delete the rendering context, you can potentially suffer the same fate as you would when you don’t manage GDI objects correctly. That is, you will fill resource memory with useless data (memory leaks) and eventually possibly crash the operating system.

View Sizing

While your application is using an OpenGL rendering context, you’ll find it important to manage sizing issues. If the user resizes the window, you must handle resizing the rendering context to match. While handling the WM_SIZE message, you may also want to reset your projection and model matrices, reset your viewport, and re-establish your viewing frustrum. You can see this is a fairly critical operation! Without managing these actions, OpenGL will not render anything at all. At best, you’ll see a view painted with the current clear color. My view class uses the code you see in Listing 29.6.

Listing 29.6 OpenGL View WM_SIZE Handler


void CGLView::OnSize(UINT nType, int cx, int cy)
{
   CView::OnSize(nType, cx, cy);

   // Change OpenGL’s rendering context size and
   // viewing frustrum.
   CClientDC dcClient(this);
   wglMakeCurrent(dcClient.m_hDC,m_hRC);
   glViewport(0,0,cx,cy);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glFrustum(-10.0,10.0,-10.0,10.0,18.0,70.0);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glTranslatef(0.0f,0.0f,-35.0f);
   wglMakeCurrent(NULL,NULL);
}

There are two areas you might want to customize. The first area involves the viewing frustrum. My OpenGL code typically works with a frustrum size you see in Listing 29.6. This means my model coordinates generally range from -10.0 to +10.0. There is no reason, however, you could not scale the frustrum by another 10 to build your models using coordinates ranging from -100.0 to ;+ 100.0. OpenGL will work fine in either case.

Adjusting the frustrum is as simple as replacing the glFrustrum() code you see in Listing 29.6 with this:

glFrustrum(-100.0,100.0,-100.0,100.0,180.0,700.0);

As you can see, I simply multiplied the values I use by a factor of 10 to create a (seemingly) larger frustrum. Remember if you adjust the frustrum size from what I use here, you must either also adjust the model coordinates accordingly or scale them appropriately when you actually render them.

The second customization area involves setting the viewport. The code in Listing 29.6 simply sets the viewport coordinates from (0,0) to (cx,cy), the new size of the client area window. However, there is nothing in my code that says this area must be square. It could be rectangular, which would distort the models when their coordinates are converted from frustrum coordinates (OpenGL) to window coordinates (Windows device context). I added a feature to the GLMinMDI sample program to enable you to experiment with rendering context sizing, the output of which is shown in Figures 29.7 and 29.8. Figure 29.7 shows the MDI child windows with their viewports set to the height and width of the client area window. Figure 29.8, on the other hand, uses the minimum of either the height or width to specify the lower right-hand corner of the viewport using the code you see in Listing 29.7.


Figure 29.7  Viewport settings with distorted output.


Figure 29.8  Viewport settings with proportional output.

Listing 29.7 OpenGL View Proportional Viewport WM_SIZE Handler


void CGLMinMDIView::OnSize(UINT nType, int cx, int cy)
{
   CGLView::OnSize(nType, cx, cy);

   // Check for proportional rendering...
   if ( m_bRenderProportional ) {
      // Reset the viewport to render proportionally
      CClientDC dcClient(this);
      wglMakeCurrent(dcClient.m_hDC,m_hRC);
      glViewport(0,0,min(cx,cy), min(cx,cy));
      wglMakeCurrent(NULL,NULL);
   } // if
}

I’ve italicized the particular line of code that handles the viewport differently. Rather than modify my base class’s code, I elected to add a WM_SIZE handler to my derived class. The base class’s WM_SIZE handler executes the code you saw in Listing 29.6. However, the derived class then has the opportunity to change things, and in this case I make the rendering context current, adjust the viewport to be proportional (if set), and make the context not current. As you can see, I force the viewport to be square by using the same coordinate value for both the right and bottom values of the lower corner. To do this, I use the minimum of either the given cx or cy, which are the width and height of the client area window, respectively. Although this serves to reduce the size of the viewing area, it does provide a nondistorted view when rendering your models.


Note:  

This is but one method of adjusting the rendering context to device context coordinate space mapping. You can also just use glOrtho() or gluPerspective() to do the same thing.




View Background Erasure

The final view handler you should provide for is background erasure, WM_ERASEBKGND. When Windows sends this message to your application, it wants you to completely repaint your background (in some or all of your window’s area). When using OpenGL, however, you want OpenGL to manage painting within its rendering context. If you simply accepted the default MFC behavior for WM_ERASEBKGND, the OpenGL view would flicker whenever you forced Windows to issue this message (such as when you resize the window). This is because Windows will first resize the window, erase the background using the windows default color (which is usually white), and then finally have OpenGL render its view. Your eyes will catch the brief moment when the screen turns to white, and then turns to the OpenGL clear color you defined when you created your rendering context (which is black for the sample programs in this chapter). To eliminate this flickering, you simply return TRUE from your WM_ ERASEBKGND handler, as I show in Listing 29.8.

Listing 29.8 OpenGL WM_ERASEBKGND Handler


BOOL CGLView::OnEraseBkgnd(CDC* pDC)
{
   // Don’t erase view...let OpenGL handle
   // that for you...
   return TRUE;
}

I added the capability to the GLMinMDI application to enable you to see and test this flickering effect. After you change the flicker handling status (turn it on or off), resize the child window to force Windows to send the WM_ERASEBKGND message. See if you see the flicker to which I refer.

View Palette Management

In some cases, such as when running on systems with 256-color video cards, you will need to manage your application’s color palette. When I say 256-color video card, what I mean is that the video card can only display 256 colors at any one time. Although this may be fine for some applications, it spells disaster for OpenGL output, which is typically rich in color (especially given lighting effects). To help solve this, you must create a custom palette and manage its selection when requested to do so by Windows.

The code I use to create and manage the palette isn’t terribly complicated, but I feel it isn’t particularly interesting to discuss here. You’d have to wade through several pages of palette theory, when I imagine what you really want to do is create a 3D scene. So, I’ll leave it to you to examine the code in any of the provided samples. I’m not trying to avoid a little extra work. I am trying to avoid clouding the true goal of this chapter, which is to introduce you to OpenGL and its capabilities. That you may (or may not) have to manage your application’s palette is an implementation detail. Simply use the code you find in the sample programs and you should have no trouble.

Integrate the Custom View Class into Your Application

Now that I’ve covered the basic steps necessary to integrate OpenGL into a view class, it is time for you to use that view class in your own applications. To do that, you will need to take these measures:

  Add the CGLView class to your project.
  Modify your existing view class to inherit from CGLView.
  Make sure that you add the required libraries to your project’s link list. (I’ll cover this later.)

The first step is easy. Just copy the GLView.cpp and GLView.h files from any of this chapter’s sample programs to your own application’s directory. Then, using Developer Studio’s menu, select Projects, Add To Project, Files to activate the Windows standard file dialog. After you’ve done this, navigate to the files’ location, select them both in the list control, and click OK. The files will now be added to your compile and link list.

The slightly more difficult chore will be to modify your existing view class to accept the CGLView class as its parent, though this is also nearly trivial after you’ve done it for the first time. To integrate the GLView into your application, you must perform these two tasks:

  Add this line to your view’s definition (.h) file:
#include “GLView.h”
  Search for, and replace, every mention of your view class’s original base class with GLView.

The first task is relatively straightforward. Because your view class will now be derived from a new base class, it makes sense that you would need to declare that base class before using it. By including CGLView’s definition file within your own view’s definition file, you not only declare your base class to the compiler, but you also provide the OpenGL header files to any other C++ class that includes your view’s definition file. This can be handy, because you no longer need to constantly include the OpenGL header files. (Note that you will still have to include the OpenGL header files for classes that don’t include your view’s definition file.)

The second task is only slightly more difficult. I suggest using the Developer Studio’s Replace function (menu option Edit, Replace) to reduce the potential number of errors this could introduce into your source code. If your current view class derives directly from CView, you will replace all occurrences of “CView” with “CGLView.” If your current view doesn’t derive directly from CView, you must follow the procedure I outlined previously to create a new OpenGL-enabled base class, and then perform the tasks I mention here. You would need to do this if your current view’s base class was CFormView, for example.

After you’ve integrated CGLView into your current view’s source files and compiled successfully, you can add your drawing code (CView::OnDraw()) to see the models you created rendered by OpenGL. This is precisely the topic I’ll address in the next section.



Build Your 3D Scene

Now that you’ve successfully integrated OpenGL into your application, it’s time for you to actually build your 3D (or 2D) scene. How you elect to render your models is completely up to you, but MFC and OpenGL will expect them to be provided when processing your OnDraw() function. The MFC AppWizard created the basic OnDraw() member for you; it simply obtained a pointer to the current document and left a TODO for you to add the appropriate drawing code. In this case, however, you must provide OpenGL with the appropriate palette (if one was created) and actually render your models. Listing 29.9 shows you one possible drawing alternative, which is the pattern I used for this chapter’s sample programs (this listing was taken from GLMin).

Listing 29.9 CGLView’s Default OnDraw() Handler


void CGLMinView::OnDraw(CDC* pDC)
{
   CGLMinDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // Set your palette
   if ( m_hPalette != NULL ) {
      SelectPalette(pDC->m_hDC, m_hPalette, FALSE);
      RealizePalette(pDC->m_hDC);
   } // if

   // Draw the OpenGL scene
   wglMakeCurrent(pDC->m_hDC, m_hRC);
   RenderView();
   SwapBuffers(pDC->m_hDC);
   wglMakeCurrent(pDC->m_hDC, NULL);
}

Here, I select and realize the palette my application created when the rendering context was created, if any. Then, I make my application’s rendering context the current one, render my view, swap the back buffer for the front, and release my rendering context (make it not current). The nice feature with this approach is that I encapsulate my model rendering code within RenderView(), thus minimizing the effects of model changes to the basic mechanics of OpenGL rendering you see here. Note if you elected not to use double-buffering (by changing the PIXELFORMATDESCRIPTOR settings in CGLView::OnCreate()), you should remove the SwapBuffers() call.

Add the OpenGL Libraries to Your Project’s Link List

Now that you have made all the previous changes to your source code, you still will find that the project will not compile and link properly. This is because you must add the OpenGL dynamic link libraries to your project’s linker settings. To do this, either right-click on the project’s name in the Workspace window and select Settings from the context menu, or select Project, Settings from the Developer Studio menu. In either case, be sure to select All Configurations in the Settings For drop list control.


Caution:  

If you forget to select All Configurations in the settings dialog, you will modify the settings for the current configuration only (which is typically set for a debug build). This will cause linker errors when you select another build type, such as for a release build.


When you have the settings dialog showing and have set the settings for all build configurations, select the Link tab. Under the General linker category, add the following to the Object/Library Modules edit control:

OpenGL32.lib GLAux.lib GLU32.lib

Now when you compile and link, the OpenGL libraries will automatically link with your project. You are ready to go!

2D and 3D Models

Up to this point, you’ve been reading the background necessary to use OpenGL. Now, it’s time to actually use it to do something interesting. I’m specifically referring to developing your models and composing a 3D scene. A model might be a shaped object, such as a sphere, or it might be an algorithm you develop to create a colorful 3D chart or graph. The methodology many graphics programmers follow is to create the basic model, or models, centered about the origin (0,0,0), and then translate them into position (or rotate, or scale, and so on). If performance is a concern, the models are translated into one or several display lists.

Basic Model Development

I’ll describe model creation by way of example. If you examine this chapter’s GLObject sample program, you will find the code contained in Listing 29.10 buried in the CGLObjectView view class.

Listing 29.10 OpenGL 3D Cube Model


void CGLObjectView::RenderSolidCube()
{
   // Draw as a wireframe (use white always)
   glDisable(GL_LIGHTING);
   glDisable(GL_LIGHT0);
   glColor3f(0.8f, 0.8f, 0.8f);
   glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_POLYGON_SMOOTH);

   // Draw your cube by hand...
   glBegin(GL_POLYGON); // front face of cube
      glVertex3f(-5.0f, 5.0f, 5.0f);
      glVertex3f(-5.0f, -5.0f, 5.0f);
      glVertex3f(5.0f, -5.0f, 5.0f);
      glVertex3f(5.0f, 5.0f, 5.0f);
   glEnd();

   glBegin(GL_POLYGON); // “back”
      glVertex3f(5.0f, 5.0f, -5.0f);
      glVertex3f(5.0f, -5.0f, -5.0f);
      glVertex3f(-5.0f, -5.0f, -5.0f);
      glVertex3f(-5.0f, 5.0f, -5.0f);
   glEnd();

   glBegin(GL_POLYGON); // “right”
      glVertex3f(5.0f, 5.0f, 5.0f);
      glVertex3f(5.0f, -5.0f, 5.0f);
      glVertex3f(5.0f, -5.0f, -5.0f);
      glVertex3f(5.0f, 5.0f, -5.0f);
   glEnd();

   glBegin(GL_POLYGON); // “left”
      glVertex3f(-5.0f, 5.0f, -5.0f);
      glVertex3f(-5.0f, -5.0f, -5.0f);
      glVertex3f(-5.0f, -5.0f, 5.0f);
      glVertex3f(-5.0f, 5.0f, 5.0f);
   glEnd();

   glBegin(GL_POLYGON); // “bottom”
      glVertex3f(-5.0f, -5.0f, 5.0f);
      glVertex3f(-5.0f, -5.0f, -5.0f);
      glVertex3f(5.0f, -5.0f, -5.0f);
      glVertex3f(5.0f, -5.0f, 5.0f);
   glEnd();

   glBegin(GL_POLYGON); // “top”
      glVertex3f(-5.0f, 5.0f, -5.0f);
      glVertex3f(-5.0f, 5.0f, 5.0f);
      glVertex3f(5.0f, 5.0f, 5.0f);
      glVertex3f(5.0f, 5.0f, -5.0f);
   glEnd();

   // Because you’re not using light, it’s hard to distinguish
   // the sides, so you’ll overlay the wireframe model to
   // make it easier to see...
   RenderWireCube();
}

In this case, I have drawn six polygons, one for each side of a cube. I could have drawn one polygon six times, translating and rotating it into place as appropriate, but this is a bit quicker because OpenGL doesn’t perform so many floating-point calculations. I’ll show you other improvements later when I discuss display lists. In any case, when you execute the GLObject program and select the solid cube object, this code will provide the OpenGL commands to render a solid cube. The cube initially is centered about the origin. If I required multiple cubes to be rendered, I would translate and rotate each into place individually using this same basic model code.

With this model, I begin by deactivating the lighting effects (I’ll present these later in this chapter), establishing the basic polygonal color (gray), setting the polygon mode, enabling depth testing, and establishing the polygon shading mode. The glEnable() function is used to establish many OpenGL settings, and through it you communicate with the rendering context. These settings remain in effect until you specifically change them by enabling another related setting or disabling the setting entirely using glDisable(). Here, I disabled lighting entirely (GL_LIGHTING), as well as light number zero specifically (GL_LIGHT0).



The polygon mode, set using glPolygonMode(), tells OpenGL how to render polygons. Here I used GL_FRONT_AND_BACK and GL_FILL. This tells OpenGL to color both the front face and the rear face of the polygon (as defined by the order of the vertex definitions) using a color fill. I could have used just GL_FRONT, because the cube is closed. However, in many cases you will use GL_FRONT_AND_BACK. The reasoning is simple after you’ve seen how OpenGL handles polygonal rendering. If you rotate the 3D scene such that you are facing the front face of a polygon, the GL_FRONT setting will cause the polygon to be filled with color. However, rotate the same view 180 degrees so you are facing the rear of the front face, and you’ll find OpenGL didn’t render your polygon at all! This is done to save rendering time by reducing calculations. For complex polygonal structures, by all means use GL_FRONT if you are sure polygons won’t disappear mysteriously from your scene. For general purpose rendering, though, the effects of the calculation overhead are typically not terribly draining, and GL_FRONT_AND_BACK mode is usually acceptable.

The polygon fill style I selected is GL_FILL. My other options are GL_POINT and GL_LINE. GL_POINT would render the polygonal vertices as points, whereas GL_LINE would render the polygon as a wireframe. Note for demonstration purposes the wireframe cube I render in the GLObject sample program is created by hand, not using polygons.

Now that you’ve created a basic model, it’s time to shift it about within your scene. To do this, you’ll have OpenGL make the many complex calculations you saw earlier in this chapter. However, you’ll soon see the true power of OpenGL—making those calculations is as easy as a single line of code!

Model Transformations

When you transform a model, you’re affecting the manner in which it is rendered by moving it, rotating it, or scaling it to meet your needs. The general recipe you’ll follow is to create your model view matrix, push it, transform your model, render your model, and then pop the model view matrix from the stack to be used again. I’ll now discuss each of the transformations individually.

Translation

When you translate, you move. Most models, for simplicity, are created at the origin. After all, it’s much easier to describe a cube when the origin sits at its center than it is if the cube is located so many units up the x-axis and so many units down the y-axis. It’s the same size cube; it’s just been moved.

Models are translated using glTranslatef():

glPushMatrix();
   // Translate this sphere into place
   glTranslatef(5.0,5.0,-5.0f);

   // Draw a sphere
   auxSolidSphere(2.0);

// Restore your matrix
glPopMatrix ();

This code renders a sphere 5 units to the right (x-axis), 5 units up (y-axis), and 5 units further back (z-axis). As you see, I save my matrix, translate, render, and then restore my matrix.

Rotation

Rotation follows a similar vein, but there is a twist. When you translate, you specify a point of translation (actually, a translation vector, but you can think of it as a point in space). You specify the x, y, and z coordinates and the object gets translated there.

Rotation, however, is different in that you are specifying rotation vectors about which your object will be rotated. For example, this code rotates a dodecahedron first about the x-axis, and then about the y-axis:

// Save your model view matrix
glPushMatrix();
   // Spin the dodec for effect
   glRotatef(fXRotate, 1.0f, 0.0f, 0.0f);
   glRotatef(fYRotate, 0.0f, 1.0f, 0.0f);

   // Draw the dodec
   auxSolidDodecahedron(2.0);

// Restore your matrix
glPopMatrix();

The previous rotation is not the same as this:

// Save your model view matrix
glPushMatrix();
   // Spin the dodec for effect
   glRotatef(fXYRotate, 1.0f, 1.0f, 0.0f);

   // Draw the dodec
   auxSolidDodecahedron(2.0);

// Restore your matrix
glPopMatrix();

The first case spun the dodecahedron about the x-axis, then the y-axis. The second case spun the dodecahedron about a vector 45 degrees between the positive x- and y-axes. You can’t mix rotations as you could translations!

In both cases, though, I used glRotatef() to perform the rotation. The first parameter is an angle of rotation, and the last three form the rotation vector about which the object will be rotated.

Scaling

When you scale an object, you’re simply enlarging or shrinking it. It’s common practice to build models to a standard size, say enclosed in a 1×1×1 cube. Then, when you need the model, you simply make it the size you want by using glScalef(). Here is an example that enlarges a cube 50%:

// Save your model view matrix
glPushMatrix();
   // Scale the cube to fit this scene size context.
   glScalef(1.50f,1.50f,1.50f);

   // Draw the cube
   auxSolidCube();

// Restore your matrix
glPopMatrix();

Note that there is no requirement to enlarge all sides equally. This code would make a cube a rectoid:

// Save your model view matrix
glPushMatrix();
   // Scale the cube to fit this scene size context.
   glScalef(5.00f,1.50f,1.50f);

   // Draw the cube
   auxSolidCube();

// Restore your matrix
glPopMatrix();

The object would be elongated (500%) on the x-axis, but it would be stretched only 50% down the y- and z-axes. If you’re using light effects (and have calculated polygonal surface normals), you may want to also scale the normals. OpenGL will manage this for you if you enable automatic normalization:

glEnable(GL_NORMALIZE);

In this case, if any distortion would affect your surface normals, OpenGL will correct those normals for you (assuming you specified them to begin with!).

Now that you’ve seen how to build models, as well as how to move them into place within your 3D scene, it’s time to think about packaging your models for ease of use and optimization. OpenGL provides a nice mechanism for doing just this—the OpenGL display list.

Display Lists

One optimization commonly used is to compile OpenGL commands into a display list. A display list is nothing more than a series of OpenGL commands rolled into the rendering context. Instead of executing your code to render a model, you tell the rendering context itself to retrieve and render one of its display lists. This is far more efficient, as the display list is stored in an OpenGL-friendly (easy to access and render) format within the rendering context.


Tip:  

It isn’t precisely accurate to say the rendering context stores the display list. The list is compiled and stored in memory where the rendering context may access it later. This facilitates list sharing, whereby you share lists among rendering contexts (assuming you have more than one available). See wglShareLists() for details.




If you only realized performance improvements by using display lists, it would probably be enough reason to use them. As it happens, though, creating a display list is no more difficult than creating your model in the first place. Two OpenGL functions, glNewList() and glEndList(), mark the beginning and ending of the display list declaration. Listing 29.11, from the GLScene sample program, shows you the same type of basic code I used earlier in the chapter to render a solid cube. With the exception of hard-coding the normal vectors, I used the typical glBegin()/glEnd() pairings to create each of the six sides. All of the cube code is now encapsulated between the display list declaration pair glNewList() and glEndList().

glNewList() marks the beginning of the display list and requires two parameters. The first, a GLuint, is simply used as the display list name (even though it’s an integer value). OpenGL will use the name to look up the list index so it might render the list when called upon to do so. In Listing 29.11, I defined the value SOLIDCUBE, the list’s name, to be the value 32. The second parameter is the mode, and it can have two values, GL_COMPILE and GL_COMPILE_AND_EXECUTE. GL_COMPILE is used to preinitialize the display list without executing the commands it contains. GL_COMPILE_AND_EXECUTE, on the other hand, not only initializes the display list but also executes the contained commands.

Listing 29.11 GLScene’s Solid Cube Display List


// Begin a display list for a solid cube
glNewList(SOLIDCUBE,GL_COMPILE);
   // You are free to place nearly any OpenGL command
   // in a display list.  You could, for example,
   // set the color here.  However, I elected to set
   // the color when I render, thus allowing me to
   // have the flexibility.
   glBegin(GL_POLYGON);
      // I could have calculated the normal vector, but
      // we know what it should be, so I simply set it
      // in this case.
      glNormal3f(0.0f,0.0f,1.0f);
      glVertex3f(1.0f,1.0f,1.0f);
      glVertex3f(-1.0f,1.0f,1.0f);
      glVertex3f(-1.0f,-1.0f,1.0f);
      glVertex3f(1.0f,-1.0f,1.0f);
   glEnd();

   glBegin(GL_POLYGON);
      glNormal3f(0.0f,0.0f,-1.0);
      glVertex3f(1.0f,1.0f,-1.0f);
      glVertex3f(1.0f,-1.0f,-1.0f);
      glVertex3f(-1.0f,-1.0f,-1.0f);
      glVertex3f(-1.0f,1.0f,-1.0f);
   glEnd();

   glBegin(GL_POLYGON);
      glNormal3f(-1.0,0.0f,0.0f);
      glVertex3f(-1.0f,1.0f,1.0f);
      glVertex3f(-1.0f,1.0f,-1.0f);
      glVertex3f(-1.0f,-1.0f,-1.0f);
      glVertex3f(-1.0f,-1.0f,1.0f);
   glEnd();

   glBegin(GL_POLYGON);
      glNormal3f(1.0,0.0f,0.0f);
      glVertex3f(1.0f,1.0f,1.0f);
      glVertex3f(1.0f,-1.0f,1.0f);
      glVertex3f(1.0f,-1.0f,-1.0f);
      glVertex3f(1.0f,1.0f,-1.0f);
   glEnd();

   glBegin(GL_POLYGON);
      glNormal3f(0.0f,1.0,0.0f);
      glVertex3f(-1.0f,1.0f,-1.0f);
      glVertex3f(-1.0f,1.0f,1.0f);
      glVertex3f(1.0f,1.0f,1.0f);
      glVertex3f(1.0f,1.0f,-1.0f);
   glEnd();

   glBegin(GL_POLYGON);
      glNormal3f(0.0f,-1.0,0.0f);
      glVertex3f(-1.0f,-1.0f,-1.0f);
      glVertex3f(1.0f,-1.0f,-1.0f);
      glVertex3f(1.0f,-1.0f,1.0f);
      glVertex3f(-1.0f,-1.0f,1.0f);
   glEnd();

glEndList(); // SOLIDCUBE

After you’ve created your display list, you render it by using the glCallList() command:

glCallList(SOLIDCUBE);

When you execute the stored command list, the state of the OpenGL rendering context will be used (and applied) to the commands. Essentially, this means any matrix work, color, work, lighting work, and so on you perform prior to calling the display list will be in effect when the stored rendering commands are issued. This cube is created at the origin and is 1×1×1 units in size. But by using glScale(), though, I could make it any size I required. In fact, I do precisely this when I create the 3D scene in the GLScene sample program. I create nine cubes, each of which is scaled, translated, and rotated individually as it is placed within the scene (see Figure 29.9).


Figure 29.9  GLScene’s program display.

The code to render the cubes in the scene is shown in Listing 29.12. As you can see, I establish the cube’s color (red), translate, rotate, and scale it before it gets rendered.

Listing 29.12 Rendering the Cube Display List


GLfloat glfMaterialColorCubes[] =
Ä{ 1.0f, 0.0f, 0.0f, 1.0f };
glMaterialfv(GL_FRONT,GL_AMBIENT_AND_DIFFUSE,
ÄglfMaterialColorCubes);

// Save your model view matrix
glPushMatrix();
   // Translate this cube into place
   glTranslatef((GLfloat)x,(GLfloat)y,5.0f);

   // Spin the cube for effect
   glRotatef (m_fYRotate, 0.0f, 1.0f, 0.0f);

   // Scale it (originates as a 1x1x1 cube, so
   // scale it to fit this scene size context).
   glScalef(1.50f,1.50f,1.50f);

   // Draw the cube
   glCallList(SOLIDCUBE);

// Restore your matrix
glPopMatrix();


Note:  

If you use the auxiliary OpenGL library to display shapes in your scene, be aware the auxiliary library will take the list names from 1 to the number of different auxiliary shapes you are using. These names, in that case, are not available for you to use. For example, GLScene uses the sphere and dodecahedron shapes, so list names 1 and 2 are not available to GLShape for locally defined display lists. If you forget and use an existing display list name, you won’t receive an error, but neither will you render the correct model.



Tip:  

A handy OpenGL command to use with display lists is glGetError(). If you execute your program and don’t see any output from the display list, it’s likely there was an OpenGL error while creating the list. By calling glGetError(), you can at least look up the error code and have some idea where to start fixing the problem. The Visual C++ 6 online help file is helpful here because it lists the possible error codes and potential causes.


When you allocate memory, you must deallocate it. It is no different with OpenGL’s display lists. You allocate display list memory using glNewList(). You must deallocate memory using glDeleteLists(). glDeleteLists() takes two parameters. The first specifies the list name to delete. The second tells OpenGL how many lists to destroy. You might destroy a single list, or you might destroy several depending upon your needs at the time.

Now that you’ve seen how to build, transform, and display your models, it’s time to bring your scene to life. I believe nothing does more for a given 3D scene than to add lighting effects, and that’s what I’ll cover next.



Light

Few things in OpenGL are more complicated than light, but then, few things can be as spectacular when you get it right. In this section I’ll explain how graphics programmers view light as a physical entity, and then I’ll show you how light is modeled in OpenGL.

If you’re a physicist, you might view light as both particle and wave. Theoretical physicists see light as a view into the universe’s past. But graphics programmers see light as a calculation. A commonly used light model calculates the light intensity at any point on the surface of an object, as shown in Figure 29.10.


Figure 29.10  A light intensity equation.

I don’t show you this equation to scare you. (I’m not even going to try to explain it in detail!) It’s here to give you an appreciation for the immense number of calculations required to render a properly shaded object given light sources. There is a lot under OpenGL’s hood. I also presented it here to ease any concerns you might have about using lit 3D scenes. Don’t worry—your first 3D scene attempt absolutely will not look right the first time. You’ll probably have to try and try again to get things just as you want them to be. Such a complicated equation is the reason behind the difficult nature of light calculations.

However, it is possible to break this equation down and tackle the individual components separately. Dividing and conquering is a perfectly valid way to master OpenGL lighting.

The Basics of Light in OpenGL

Before I crack the light equation and begin flinging imaginary photons, you should know the mechanics of light and how things are described when using OpenGL. Using light sources in OpenGL is as easy as enabling certain settings in your rendering context. Which settings and how they are applied is the secret.

Before OpenGL will calculate lighting effects, you must enable the OpenGL lighting state. To do this, you use glEnable() with the GL_LIGHTING parameter. glDisable() with the same parameter deactivates the lighting state. This acts as a global circuit breaker for the light model. If GL_LIGHTING is disabled, none of your models will show the effects of light. In general, you’ll also want to make sure GL_DEPTH_TEST is enabled. This causes OpenGL to make depth calculations when rendering polygons so objects closer to the eye point properly obscure more distant objects. Light serves to enhance that illusion.

OpenGL provides you with a total of eight light sources. These eight light sources, GL_LIGHT0 through GL_LIGHT7, may either be distant light sources, like the sun, or point light sources, such as lamps or spotlights. To “turn on” one of these light sources, you use glEnable() with the OpenGL enumeration (GL_LIGHT0, and so on) as the parameter. Similarly, you “turn off” the light source using glDisable(). Easy so far.

Each of the eight light sources has a position in 3D space. The position is recorded in the usual (x,y,z,w) form, but the w parameter is used to dictate the type of light source, positional (non-zero) or directional (0.0). Directional light is light from a far distant source that travels parallel to the vector formed by the given (x,y,z) values. Positional light, on the other hand, is more like a lamp. In that case, the (x,y,z) coordinate is the location of the lamp in 3D space.

If you are using a positional light, you have control over many of the light’s properties. For example, you might specify the light source as a spotlight with a given cutoff angle or attenuation factor. See glLightfv() in the online help for more details, though I use a positional light in the GLObject sample program included with this chapter.

Types of Light

The light equation I presented earlier is composed of several terms. Each term in the equation represents a type of light. The sum intensity is the total of the terms in the equation; therefore, each type of light contributes some partial light intensity to the whole. By adding or removing the equation’s terms, I modify a given object’s appearance when OpenGL renders it. This, now, is the hard part. Getting this blend of light terms correct for your particular scene will usually be the challenge.

Ambient Light

The first term of the equation represents the ambient light present in your scene. The fact that it stands outside the summation tells you it is constant no matter how many additional light sources are present to illuminate your scene. Ambient light has this basic property: all of the light rays strike the object in parallel.

To see an example of ambient light, simply walk outside. If it’s a sunny day, there is plenty of ambient light (from the sun). If it’s a cloudy or rainy day, the ambient term is relatively smaller than it was on the sunny day, so there is less total light available to your eyes (even to the point you lose color perception). If it’s night, there is probably very little ambient light available except on a clear night with a full moon.

Ambient light, and actually all of the other various types of light, is composed of red, green, and blue color intensities. If each of the three colors is present at roughly equal levels, the resulting light is white (or gray, if the overall intensity is low). If any one is greater than the others, that color will begin to dominate, and your scene will have additional color intensity in that color range. That is, if the ambient light is predominantly red, your objects will appear reddish, or at least have a reddish tint.

You establish the basic ambient color when you use the glLightfv() command:

GLfloat fLightAmbient0[4] { 1.0f, 1.0f, 1.0f, 1.0f };
glLightfv(GL_LIGHT0,GL_AMBIENT, fLightAmbient0);

Here, I specify which light I refer to (GL_LIGHT0), the type of light I want to adjust (GL_AMBIENT), and what the color intensities for the light are (fLightAmbient0).

Diffuse Light

Diffuse light forms the second term in the equation (the first term after the summation). This term is a vectored term, so the position of the light affects how the object appears. This is the first term where the surface normal is critical. If the light strikes the object parallel to the surface normal, you get the most intense reflection from the object. Light that strikes perpendicular (at 90 degrees) to the surface normal yields no additional light intensity at that point on the object’s surface.

To see diffuse light in effect, take a dull object (a rubber basketball, for example) into a dark room and shine a flashlight upon its surface from a short distance. The surface of the basketball is illuminated most brightly at the point to which the flashlight is aimed. Surface points radiating away from there are not illuminated as brightly. The sides of the basketball aren’t lit at all (a location about where your hand would be if you held the ball in one hand and the flashlight in the other). At this point, the light rays are coming in at 90 degrees to the basketball, so they contribute no light intensity there.

You can specify your light source have a diffuse component by using the glLightfv() command again:

GLfloat fLightDiffuse0[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
glLightfv(GL_LIGHT0,GL_DIFFUSE,fLightDiffuse0);

These commands establish the diffuse term for light zero’s intensity contribution to the overall diffuse tendencies of the objects it illuminates. Brighter lights use higher values for the components (the values in fLightDiffuse0). Again, there is no requirement that the individual color intensities be equal. You could have more blue, for example, shining on your objects if that’s the effect you sought.



Specular Light

The final term in the light equation is the specular content. Specular light imprints a mirror image of the light source onto the surface of the object (or tries to; it depends on the object’s material properties as well). Essentially, specular light makes objects appear shiny.

If you were to take a shiny metal ball into your dark room with your flashlight instead of a basketball, you would see the specular light component as a shiny spot on the surface of the metal ball directly parallel to the flashlight’s beam. It’s usually seen as a bright white spot on an otherwise colored object. The spot is intensely white at the point where the light strikes parallel to the surface normal, but it attenuates quickly as the surface normal’s angle rolls off towards the perpendicular of the surface vector.

As with the other light components, you establish a specular content to your light using glLightfv():

fLightSpecular0[0] = { 1.0f, 1.0f, 1.0f, 1.0f };
glLightfv(GL_LIGHT0,GL_SPECULAR,fLightSpecular0);

Specular light, and to a greater or lesser degree, diffuse light illuminate materials differently depend on the material they are striking. Some objects appear dull no matter how much light you shine upon them, whereas others reflect brilliantly with almost no light. This effect is controlled by the material properties of the object in question.

Object Material Properties

Just as light sources have different light intensity content, objects reflect different components of light differently depending upon their individual material properties. A chrome object will reflect far more light than dirt or grass. Further, chrome objects, and mirrors in general, reflect the incoming light perfectly, or nearly so. Grass and dirt, on the other hand, only reflect certain color intensities no matter what color of light is applied. The grass is greener than chrome because grass absorbs all incoming light spectra but green. Chrome reflects it all.

Therefore, it may not be surprising to find you have material properties similar to the light source properties you specified earlier. A given object reflects ambient, diffuse, and spectral light. It may also emit light as well.


Tip:  

It may be helpful to remember this when dealing with light in OpenGL that polygons don’t have color. Rather, they reflect light of certain color intensities. If your object’s material reflects mostly red ambient light, it will appear red when rendered in ambient light with some red light intensity.


Setting Object Material Properties

Setting an individual object’s material properties is quite similar to setting a light source’s light properties. However, instead of using glLightfv(), you would use glMaterialfv(). For example, to have an object appear blue (reflect blue ambient light), you might use code similar to this:

GLfloat fMaterialAmbient[4] = { 0.0f, 0.0f, 1.0f, 1.0f };
glMaterialfv(GL_FRONT,GL_AMBIENT_AND_DIFFUSE,fMaterialAmbient);

In this case, the object would reflect all blue light. If your light source contained some blue light, you would see this object. If not (you had a red light source), the object would appear black if you saw it at all.

Color Tracking

To ease things somewhat, OpenGL implements color tracking, which you can enable or not as your requirements dictate. You may set the individual material properties for a given object, as you saw in the last section, or you can elect to track the light source’s color. To enable color tracking, use these OpenGL commands:

glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT,GL_AMBIENT_AND_DIFFUSE);

Now, you may simply specify a color for your polygons using glColor(). OpenGL handles the calculations for you. Although you can get very precise results by not using color tracking, color tracking has the advantage OpenGL executes faster when color tracking is enabled. This becomes critical when you start dealing with objects with many, many polygons.

Both the GLObject and GLScene sample programs implement lighting effects. In the next section, I’ll turn to another effect you can employ to give your objects the illusion of reality—texture mapping.

Texture Mapping

Texture mapping is a process where you apply a bitmap (the decal) to a polygon to make the polygon appear more realistic. In this manner you seemingly exceed the capabilities of OpenGL polygon rendering to create stunning scenes. Figure 29.11 shows you a texture-mapped cube from the GLTexCube sample program.


Figure 29.11  Texture-mapped (spinning) cube.

OpenGL Images and Windows Device-Independent Bitmaps

Unfortunately, not all computer systems store and display images in the same manner. As OpenGL strives to be portable, it had to implement its own form of image storage. Not too surprisingly, this storage format doesn’t correspond with the Windows format, the Device Independent Bitmap or DIB.

When you use OpenGL texture mapping, you must perform three steps:

1.  Load your Windows image into memory.
2.  Map (copy) your Windows image into OpenGL’s representation.
3.  Apply the texture to a polygon.

I’ve encapsulated the first step in a class you can simply use. The details of Windows DIB storage are beyond the scope of this chapter; simply use the CDIBHolder class as I’ve shown in the GLTextCube sample program to contain your bitmap. The class has a member that takes a filename, and from there it loads the file, extracts what it requires, and saves the DIB in memory until you need it.

The more interesting step, at least for this section, is step two. You’ll look at the final step, applying the texture, in the next section. Here, though, you need to map the DIB’s storage format to OpenGL’s image storage format. To do that, you will create a color table and use glPixelMapfv() to actually copy the colors from the DIB to your rendering context. Then when you actually apply the texture, the color map in the rendering context will handle the color associations for you. Listing 29.13 shows how to map colors.

Listing 29.13 DIB to OpenGL Color Mapping


void CGLTexCubeView::InitColorTables()
{
   LPRGBQUAD pColorTable = m_CDIBHolder.GetDIBRGBTable();

   // Copy the colors used by the DIB to class local
   // storage.
   for( int i = 0; i <= 255; ++i ) {
      m_rgbRed[i] = (GLfloat)pColorTable[i].rgbRed / 255;
      m_rgbGreen[i] = (GLfloat)pColorTable[i].rgbGreen / 255;
      m_rgbBlue[i] = (GLfloat)pColorTable[i].rgbBlue / 255;
   } // for

   // Map the colors for OpenGL to use
   CClientDC dcClient(this);
   wglMakeCurrent(dcClient.m_hDC, m_hRC);
   glPixelMapfv(GL_PIXEL_MAP_I_TO_R,256,m_rgbRed);
   glPixelMapfv(GL_PIXEL_MAP_I_TO_G,256,m_rgbGreen);
   glPixelMapfv(GL_PIXEL_MAP_I_TO_B,256,m_rgbBlue);
   glPixelTransferi(GL_MAP_COLOR,TRUE);

   wglMakeCurrent(dcClient.m_hDC, m_hRC);
}




Note:  

OpenGL requires the texture image to have a width and height that is a power of 2. Also, because of the mapping mode I used, your images must be composed of 256 colors (no more, no less).


After you’ve loaded a bitmap file, you can extract the image’s color information. This information is stored in the RGB table—which was retrieved using CDIBHolder:: GetDIBRGBTable(). You then build red, green, and blue color arrays to store the individual colors used to display the bitmap. Then, after making the rendering context current, you actually map the DIB colors to OpenGL’s color mapping table using glPixelMapfv(). After you’ve mapped the red, green, and blue colors, you call glPixelTransferi() to establish the pixel transfer mode you will use when you actually apply the texture to the polygon. When I say pixel transfer, I mean just that—pixels will be transferred from the DIB storage to the rendering context as determined by the pixel transfer mode. Here, you’re doing a simple color mapping. Because you established the color map using the original DIB colors, the resulting texture should appear to be the same as the original DIB. There is no requirement to map the colors in this manner, but this is the most common use of color mapping in OpenGL. Now I’ll apply the texture to a polygon.

Applying Texture

At this point, you have a bitmap loaded into memory and a color map established to marry the DIB’s colors to your OpenGL rendering context. Now it is time to apply the texture. To do this, you locate the DIB’s data in memory, as well as determine the width and height of the image, and then call OpenGL’s glTexImage2D() command to create a OpenGL image (see Listing 29.14). The image is applied to your polygon when it is rendered (in this case I use a display list).

Listing 29.14 Rendering a Textured Polygon


void CGLTexCubeView::RenderView()
{
   GLvoid* pTextureBits = (GLvoid*) m_CDIBHolder.GetDIBBits();
   GLint iDIBWidth = m_CDIBHolder.Width();
   GLint iDIBHeight = m_CDIBHolder.Height();

   glTexImage2D(GL_TEXTURE_2D,0,3,iDIBWidth,iDIBHeight,
   0,GL_COLOR_INDEX,GL_UNSIGNED_BYTE,pTextureBits);

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   glPushMatrix();
      // Scale it a bit
      glScalef(10.0f, 10.0f, 10.0f);

      // Rotate it
      glRotatef(m_yRotate,0.0f,1.0f,1.0f);

      // Render it
      glCallList(TEXCUBE);
   glPopMatrix();

   glFlush();
}

You tell OpenGL how to apply the texture to the polygon by specifying a texture coordinate with each of your polygon’s coordinates:

glBegin(GL_POLYGON); // “front”
   glTexCoord2f(0.0f, 1.0f);
   glVertex3f(-1.0f, 1.0f, 1.0f);
   glTexCoord2f(0.0f, 0.0f);
   glVertex3f(-1.0f, -1.0f, 1.0f);
   glTexCoord2f(1.0f, 0.0f);
   glVertex3f(1.0f, -1.0f, 1.0f);
   glTexCoord2f(1.0f, 1.0f);
   glVertex3f(1.0f, 1.0f, 1.0f);
glEnd();

Here I create a cube face, and with each cube vertex, I assign a texture vertex. When OpenGL encounters the vertex mapping (when rendering the cube), it will transfer the pixels from the image it has (created using glTexImage2D()) to the rendering context, thus filling your polygon with the textured image. For the final OpenGL topic, I’ll present a couple of special effects you can use to enhance your graphical scene—alpha blending and fog.

Special Effects

OpenGL is capable of rendering many interesting effects, and I’d like to briefly touch on two of them here. You can see both of these implemented in the GLScene sample program included with this chapter. The first, blending, is a mechanism to make objects transparent, or at least increase and decrease their relative transparency. The second cool effect is fog. Fog blends your objects into the specified fog color, thus making them disappear into the fog over some distance.

Blending

Blending, or specifically alpha blending, is used to render objects with some amount of transparency. You may have noticed on occasion in this chapter that I used color values with four components. The first three were the standard red, green, and blue. Nothing particularly noteworthy there. The fourth color value, however, was the alpha value. It ranges from 0 to 1, with 0 being invisible and 1 being totally visible. Values somewhere in between allow the object to be seen, but objects that would normally be culled (not seen because they would normally be hidden) will be seen through the alpha object. The “blending” comes into play because the colors of the background objects are blended into the colors of the foreground objects—how much blending will depend upon the alpha setting for each object.



You enable blending by using glEnable() with the GL_BLEND parameter and by specifying a blending function:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

There are many settings for the OpenGL glBlendFunc() command, but what I’ve shown you here is usually the best combination, for theoretical reasons. (If you’re interested, see glBlendFunc() in the online documentation). The resulting blending function is the most precise and one of the fastest true blending functions (there are trivial cases that would be faster).

In any case, after you’ve enabled blending, it’s best to render your objects from rear to front. This will result in the best possible resulting color being transferred to your window. Any object rendered after blending is activated has blending applied, and blending remains in effect until you disable it. Figure 29.12 shows you the GLScene example with the alpha blended cube active.


Figure 29.12  GLScene with alpha blended cube activated.

Fog

If you are interested in simulating a true 3D scene, such as an outdoor scene with weather, you can tell OpenGL to apply a fog effect that makes the scene appear as if it were really in a foggy weather pattern. It’s also quite easy to use. You simply specify a fog color (typically white or light gray) and the type of fog you want to apply. Then, you let OpenGL do the hard work of remapping the individual object color to the fog color, as you see in Figure 29.13.


Figure 29.13  GLScene with fog activated.

OpenGL offers three types of fog—linear, exponential rolloff, and exponential squared rolloff. That is, OpenGL will automatically change the colors of the objects it renders to take into account fog effects. You tell OpenGL what type of fog effect you want.

With linear fog, OpenGL applies the fog color evenly to all objects regardless of their distance from the eye point (the imaginary location from where you are viewing the scene). This would be fine for thick fog, but otherwise it isn’t too realistic (it may be useful for simulating other effects, such as dimming the light available to the scene). It is less computationally intensive, however.

A more realistic fog effect, though, is exponential rolloff. In this case, distant objects blend into the fog (and eventually disappear), but closer objects retain some of their color. The closer they are to the eye point, the more of their color they retain. The difference between the exponential rolloff and the exponential squared rolloff is that the squared version shows a more pronounced effect. I elected to use the squared version to demonstrate fog effects in the GLScene sample program.

Using fog effects is very straightforward:

GLfloat m_fFogColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
glEnable(GL_FOG);
glFogf(GL_FOG_DENSITY,0.03f);
glFogi(GL_FOG_MODE,GL_EXP2);
glFogfv(GL_FOG_COLOR,m_fFogColor);
glClearColor(0.6f,0.6f,0.6f,0.0f); // gray

Here I enable fog using glEnable(), set the fog density (how thick it is), set the mode (GL_EXP2 for more pronounced effect) and set the fog color (white, in this case). OpenGL will blend the colors of the objects, near to far, with the fog color. Closer objects will have less of the fog color blended in than far objects, which will be rendered completely white if they’re distant enough. Because fog has no effect on the background, I also set the background clear color to light gray. If I didn’t do this, the background would remain black while the objects were rendered with fog, which doesn’t look quite right.

Summary

With this, you have completed your look at OpenGL using MFC. Although the samples I’ve presented are quite simplified, the capabilities OpenGL offers are stunning in the extreme. Armed with some theoretical background and a few relatively modest function calls, OpenGL can become as simple as a tool to render 3D scenes or as complex as a 3D flight simulation. The possibilities are truly limitless, as many 3D games on the contemporary game market suggest. However, there is more to the 3D story. In the next chapter, you’ll examine Microsoft’s version of a high-performance graphics programmer’s interface, DirectX.